#include "game.h"

#include <QDebug>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>

#include <utility>

Game::Game(int mapWidth, int mapHeight)
{
    int w = mapWidth >= minimumMapWidth ? mapWidth : defaultMapWidth;
    int h = mapHeight >= minimumMapHeight ? mapHeight : defaultMapHeight;
    mapSize.setWidth(w);
    mapSize.setHeight(h);

    reset();
}

void Game::reset()
{
    state = State::initial;
    dir = lastDir = Direction::right;

    // place snake at the middle of the map
    QPoint head, body;
    int w = mapSize.width(), h = mapSize.height();
    body.setX(w / 2 - 1);
    body.setY(h / 2);
    head.setX(w / 2);
    head.setY(h / 2);

    snake.clear();
    snake.push_back(head);
    snake.push_back(body);

    obstacles.clear();

    generateFood();

    score = 0;
    time = 0;
    growing = 0;
}

void Game::ready()
{
    auto prevState = state;
    state = State::ready;

    qDebug("Ready");

    emit gameStateChanged(prevState, state);
}

void Game::start()
{
    auto prevState = state;
    state = State::started;

    qDebug("Game Start!");
    timerId = startTimer(updateInterval);

    emit gameStateChanged(prevState, state);
}

void Game::pause()
{
    auto prevState = state;
    state = State::paused;

    killTimer(timerId);
    qDebug("Game Paused");

    emit gameStateChanged(prevState, state);
}

void Game::over()
{
    auto prevState = state;
    state = State::over;

    killTimer(timerId);
    qDebug("Game Over!");

    emit gameStateChanged(prevState, state);
}

void Game::restart()
{
    auto prevState = state;
    reset();

    emit gameStateChanged(prevState, state);
}

bool Game::save(const QString& fileName)
{
    QJsonObject g;
    g.insert("state", int(state));
    g.insert("dir", int(dir));
    g.insert("lastDir", int(lastDir));
    g.insert("score", score);
    g.insert("time", time);
    g.insert("growing", growing);

    g.insert("mapSize", QJsonArray{mapSize.width(), mapSize.height()});
    g.insert("food", QJsonArray{food.x(), food.y()});

    QJsonArray jSnake, jObs;
    for (const auto& pt : snake)
        jSnake.push_back(QJsonArray{pt.x(), pt.y()});
    for (const auto& pt : obstacles)
        jObs.push_back(QJsonArray{pt.x(), pt.y()});

    g.insert("snake", jSnake);
    g.insert("obstacles", jObs);

    QJsonDocument jsonDoc(g);
    QByteArray data = jsonDoc.toJson(QJsonDocument::Compact);
    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly)) {
        qDebug() << "unable to open file " << fileName;
        return false;
    }
    file.write(data);
    file.close();
    return true;
}

bool Game::load(const QString& fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        qDebug() << "unable to open file " << fileName;
        return false;
    }

    QByteArray data = file.readAll();
    file.close();
    QJsonParseError e;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &e);
    if (e.error != QJsonParseError::NoError) {
        qDebug("json parse error occurred");
        return false;
    }

    std::set<QPoint> tObs;
    std::deque<QPoint> tSnake;
    QPoint tFood, tMapSize;
    int tDir, tLastDir, tState;

    auto parsePoint = [](const QJsonValueRef& v, QPoint& out) -> bool {
        if (!v.isArray()) return false;
        auto a = v.toArray();
        if (a.size() != 2) return false;
        if (!a[0].isDouble() || !a[1].isDouble()) return false;
        out.setX(a[0].toInt());
        out.setY(a[1].toInt());
        return true;
    };

    QJsonObject g = jsonDoc.object();
    if (!g["state"].isDouble() || !g["dir"].isDouble() || !g["score"].isDouble() ||
        !g["time"].isDouble() || !g["growing"].isDouble() || !g["lastDir"].isDouble())
        return false;
    if (!g["obstacles"].isArray() || !g["snake"].isArray())
        return false;
    if (!parsePoint(g["food"], tFood) || !parsePoint(g["mapSize"], tMapSize))
        return false;

    tState = g["state"].toInt();
    tDir = g["dir"].toInt();
    tLastDir = g["lastDir"].toInt();
    if (tState < 0 || tState >= int(State::numStates))
        return false;
    if (tDir < 0 || tDir >= int(Direction::numDirections))
        return false;
    if (tLastDir < 0 || tLastDir >= int(Direction::numDirections))
        return false;

    QPoint p;
    for (const auto& v : g["snake"].toArray()) {
        if (!parsePoint(v, p))
            return false;
        tSnake.push_back(p);
    }
    for (const auto& v : g["obstacles"].toArray()) {
        if (!parsePoint(v, p))
            return false;
        tObs.insert(p);
    }

    // check is done. load values
    auto prevState = state;
    state = static_cast<State>(tState);
    dir = static_cast<Direction>(tDir);
    lastDir = static_cast<Direction>(tLastDir);
    score = g["score"].toInt();
    time = g["time"].toInt();
    growing = g["growing"].toInt();
    food = tFood;
    mapSize = QSize(tMapSize.x(), tMapSize.y());
    snake = std::move(tSnake);
    obstacles = std::move(tObs);

    emit gameStateChanged(prevState, state);
    emit gameUpdate();
    return true;
}

void Game::addObstacleAt(int i, int j)
{
    obstacles.insert(QPoint(i, j));
}

void Game::removeObstacleAt(int i, int j)
{
    obstacles.erase(QPoint(i, j));
}

bool Game::hasObstacleAt(int i, int j) const
{
    return obstacles.count(QPoint(i, j));
}

void Game::timerEvent(QTimerEvent *event)
{
    Q_UNUSED(event);

    time++;

    if (dir == oppositeDirection(lastDir))
        dir = lastDir; // avoid fast key press bug
    lastDir = dir;

    QPoint nextHead = getNextPosition(snake.front(), dir);
    if (!checkPosition(nextHead)) {
        over();
        return;
    }

    snake.pop_back();
    snake.push_front(nextHead);

    bool gotFood = (food == nextHead);
    if (gotFood)
        growing += 3;

    if (growing > 0) {
        growing--;
        QPoint newHead = getNextPosition(nextHead, dir);
        if (!checkPosition(newHead)) {
            over();
            return;
        }
        snake.push_front(newHead);
    }

    if (gotFood) {
        score++;
        bool foodGenerated = generateFood();
        if (!foodGenerated) {
            over();
            return;
        }
    }

    emit gameUpdate();
}

/**
 * @brief Game::checkPosition
 * @param p
 * @return true if point p is valid
 */
bool Game::checkPosition(const QPoint &p) const
{
    int x = p.x(), y = p.y();
    if (x < 0 || x >= mapSize.width() || y < 0 || y >= mapSize.height())
        return false;
    if (obstacles.count(p))
        return false;
    if (collideWithSnake(p))
        return false;
    return true;
}

bool Game::collideWithSnake(const QPoint &p) const
{
    for (const auto& pt : snake)
        if (p == pt)
            return true;
    return false;
}

QPoint Game::getNextPosition(const QPoint &p, Direction d)
{
    QPoint ret = p;
    switch (d) {
    case Direction::up:
        ret.ry() -= 1; break;
    case Direction::down:
        ret.ry() += 1; break;
    case Direction::left:
        ret.rx() -= 1; break;
    case Direction::right:
        ret.rx() += 1; break;
    default: break;
    }
    return ret;
}

/**
 * @brief Game::generateFood
 * @param minDistance
 * @param maxTryTimes
 * @return true if food is generated
 * @todo qrand() is obsolete, use other random number generators instead
 */
bool Game::generateFood(int minDistance, int maxTryTimes)
{
    int count = 0;
    do {
        food.setX(qrand() % mapSize.width());
        food.setY(qrand() % mapSize.height());
        if (++count > maxTryTimes)
            break;
    } while (collideWithSnake(food) || obstacles.count(food) ||
             (food - snake.front()).manhattanLength() < minDistance);

    if (count > maxTryTimes) {
        for (int i = 0; i < mapSize.width(); ++i) {
            for (int j = 0; j < mapSize.height(); ++j) {
                food.setX(i);
                food.setY(j);
                if (!collideWithSnake(food) && !obstacles.count(food))
                    return true;
            }
        }
        return false;
    } else {
        return true;
    }
}

void Game::setSnakeDirection(Direction d)
{
    if (d != oppositeDirection(dir))
        dir = d;
}
